package server

import (
	"fmt"
	"io/ioutil"
	"log"
	"os"
	"path"
	"regexp"
	"strings"

	"gitee.com/shuokeyun/kratos/cmd/sk-kratos/internal/base"
	"github.com/AlecAivazis/survey/v2"
	"golang.org/x/mod/modfile"

	"github.com/emicklei/proto"
	"github.com/spf13/cobra"
)

// CmdServer the service command.
var CmdServer = &cobra.Command{
	Use:   "server",
	Short: "Generate the proto Server implementations",
	Long:  "Generate the proto Server implementations. Example: kratos proto server api/xxx.proto -target-dir=internal/service",
	Run:   run,
}
var targetDir string

func init() {
	CmdServer.Flags().StringVarP(&targetDir, "target-dir", "t", "internal/service", "generate target directory")
}

func run(cmd *cobra.Command, args []string) {
	if len(args) == 0 {
		fmt.Fprintln(os.Stderr, "Please specify the proto file. Example: kratos proto server api/xxx.proto")
		return
	}
	protoFile := args[0]
	if !strings.HasSuffix(args[0], ".proto") {
		protoFile = getServerFilePath(args[0])
	}
	reader, err := os.Open(protoFile)
	if err != nil {
		log.Fatal(err)
	}
	defer reader.Close()

	parser := proto.NewParser(reader)
	definition, err := parser.Parse()
	if err != nil {
		log.Fatal(err)
	}

	var (
		pkg string
		res []*Service
	)
	nName := modName()
	li := strings.Split(nName, "/")
	domain := ""
	if len(li) > 1 && strings.HasSuffix(li[0], ".com") {
		nName = strings.Join(li[1:], "/")
		domain = li[0]
	}
	proto.Walk(definition,
		proto.WithOption(func(o *proto.Option) {
			if o.Name == "go_package" {
				pkg = path.Join(domain, strings.Split(o.Constant.Source, ";")[0])
			}
		}),
		proto.WithService(func(s *proto.Service) {
			cs := &Service{
				Package: pkg,
				Service: s.Name,
			}
			var sDocLines []string
			if s.Doc() != nil && len(s.Doc().Lines) > 0 {
				sDocLines = s.Doc().Lines
				for k, v := range sDocLines {
					sDocLines[k] = strings.TrimSpace(v)
				}
				if strings.Index(sDocLines[0], s.Name) != 0 {
					sDocLines[0] = s.Name + "Service " + sDocLines[0]
				}
			} else {
				sDocLines = []string{
					s.Name + "Service .",
				}
			}
			cs.Doc = sDocLines
			for _, e := range s.Elements {
				r, ok := e.(*proto.RPC)
				if ok {
					var docLines []string
					if r.Doc() != nil && len(r.Doc().Lines) > 0 {
						docLines = r.Doc().Lines
						for k, v := range docLines {
							docLines[k] = strings.TrimSpace(v)
						}
						if strings.Index(docLines[0], r.Name) != 0 {
							docLines[0] = r.Name + " " + docLines[0]
						}
					} else {
						docLines = []string{
							r.Name + " .",
						}
					}
					cs.Methods = append(cs.Methods, &Method{Service: s.Name, Name: r.Name, Request: r.RequestType,
						Reply: r.ReturnsType, Type: getMethodType(r.StreamsRequest, r.StreamsReturns), Doc: docLines})
				}
			}
			res = append(res, cs)
		}),
	)
	if targetDir == "" {
		targetDir = "internal/service"
	}
	reg := regexp.MustCompile(`v[0-9]+`)
	for _, s := range res {
		if i := strings.Index(s.Package, "/api/"); i > 0 {
			arr := strings.Split(s.Package[i+5:], "/")
			if len(arr) < 2 {
				s.TargetDir = targetDir
			} else {
				arr = arr[len(arr)-2:]
				if reg.Match([]byte(arr[1])) {
					s.TargetDir = path.Join("internal", arr[0], "service")
				} else {
					s.TargetDir = targetDir
				}
			}
			if _, err := os.Stat(s.TargetDir); os.IsNotExist(err) {
				if err := os.MkdirAll(s.TargetDir, 0700); err != nil {
					log.Fatal(err)
				}
			}
		}
	}
	for _, s := range res {
		to := path.Join(s.TargetDir, strings.ToLower(base.Camel2Case(s.Service))+".go")
		if _, err := os.Stat(to); !os.IsNotExist(err) {
			fmt.Fprintf(os.Stderr, "%s already exists: %s\n", s.Service, to)
			continue
		}
		b, err := s.execute()
		if err != nil {
			log.Fatal(err)
		}
		if err := os.WriteFile(to, b, 0o644); err != nil {
			log.Fatal(err)
		}
		fmt.Println(to)
	}
}

func modName() string {
	modBytes, err := ioutil.ReadFile("go.mod")
	if err != nil {
		if modBytes, err = ioutil.ReadFile("../go.mod"); err != nil {
			return ""
		}
	}
	return modfile.ModulePath(modBytes)
}

func getMethodType(streamsRequest, streamsReturns bool) MethodType {
	if !streamsRequest && !streamsReturns {
		return unaryType
	} else if streamsRequest && streamsReturns {
		return twoWayStreamsType
	} else if streamsRequest {
		return requestStreamsType
	} else if streamsReturns {
		return returnsStreamsType
	}
	return unaryType
}

func getServerFilePath(serviceName string) string {
	m := modName()
	serviceName = base.Camel2Case(serviceName)
	protoName := getFileNamePrefix(m) + serviceName + ".proto"
	paths := readProtoFilePath("api", protoName)
	if len(paths) > 1 {
		var sel string
		survey.AskOne(&survey.Select{
			Message: "select proto file",
			Options: paths,
		}, &sel)
		if sel == "" {
			os.Exit(0)
		}
		return sel
	}
	if len(paths) == 0 {
		log.Fatalf("The file %s was not found", protoName)
	}
	return paths[0]
}

func readProtoFilePath(p, protoName string) []string {
	var paths []string
	if protoExists(p, protoName) {
		paths = []string{path.Join(p, protoName)}
	}
	dirs, err := ioutil.ReadDir(p)
	if err != nil {
		log.Fatalf("%s directory does not exist", p)
	}
	for _, v := range dirs {
		if v.IsDir() {
			paths = append(paths, readProtoFilePath(path.Join(p, v.Name()), protoName)...)
		}
	}
	return paths
}

func protoExists(dir, name string) bool {
	_, err := os.Stat(path.Join(dir, name))
	return err == nil
}

func getFileNamePrefix(name string) string {
	li := strings.Split(name, "/")
	reg := regexp.MustCompile(`v[0-9]+`)
	sname := ""
	for i := len(li) - 1; i >= 0; i-- {
		if !reg.Match([]byte(li[i])) {
			sname = li[i]
			break
		}
	}
	return strings.Replace(sname, "-", "_", -1) + "_"
}
